package com.gopher.gopherbeasts;

import java.util.ArrayList;
import java.util.HashMap;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Vector2;

// World is basically the main game simulation
// handles entities, level data, and collisions
// it also renders everything... might need splitting
public class World {

	public enum WorldState {
		RUNNING, NEXT_LEVEL, GAME_OVER
	}

	public static final int TILE_SIZE = 32;

	public static final int SCREEN_W = 25 * TILE_SIZE; // 800 pixels
	public static final int SCREEN_H = 15 * TILE_SIZE; // 480 pixels

	public static final Vector2 GRAVITY = new Vector2(0, -1f);

	private byte[][] tiles;
	public int worldWidth, worldHeight;

	private SpriteBatch batch;
	private ShapeRenderer boundsChecker;
	private OrthographicCamera ocam;

	// the two arraylists that keep track of game entities
	public ArrayList<Entity> entities = new ArrayList<Entity>();
	private ArrayList<Entity> entities_buffer = new ArrayList<Entity>();

	public Player p;

	public WorldState state;
	public ArrayList<Vector2> visualizer = new ArrayList<Vector2>();

	public World(SpriteBatch batch) {
		this.batch = batch;
		boundsChecker = new ShapeRenderer();
		ocam = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		loadLevel("images/world/level2.png");

		// 24 is a pretty good height and width
		// player can be made any size though and collisions will work
		p = new Player(128, 200, 24, 24, this);
		entities.add(p);
	}

	public void update() {

		for (Entity e : entities) {
			// if alive, update
			if (e.isAlive()) {
				e.move();
				if (e.collidesWithMap()) {
					collideWithMap(e);
				}
				e.update();
				checkCollisions();
			}
			// if still alive, keep on list
			if (e.isAlive()) {
				entities_buffer.add(e);
			}
		}

		// swap references with entities and the buffer, then clear buffer
		ArrayList<Entity> temp = entities;
		entities = entities_buffer;
		entities_buffer = temp;
		entities_buffer.clear();

	}

	Matrix4 matrix = new Matrix4();
	public void render(float interpolation) {
		// matrix.idt();
		// matrix.setToTranslation(p.pos.x, p.pos.y, 0);
		// batch.setTransformMatrix(matrix);

		Vector2 t = new Vector2(p.ox, p.oy);
		t.lerp(new Vector2(p.x, p.y), interpolation);
		t.x = Math.round(t.x + p.w / 2);
		t.y = Math.round(t.y + p.h / 2);

		// float gfxw = Gdx.graphics.getWidth();
		// float gfxh = Gdx.graphics.getHeight();

		t.x = Mth.clamp(t.x, SCREEN_W / 2, worldWidth - SCREEN_W / 2);
		t.y = Mth.clamp(t.y, SCREEN_H / 2, worldHeight - SCREEN_H / 2);

		ocam.position.set(t.x, t.y, 0);
		ocam.update();
		batch.setProjectionMatrix(ocam.combined);
		boundsChecker.setProjectionMatrix(ocam.combined);

		batch.begin();
		batch.disableBlending();
		// batch.draw(Art.background, 0, 0, tiles.length * TILE_SIZE, tiles[0].length * TILE_SIZE);

		float scale = 8f;
		float bgWidth = Art.background.getWidth() * scale;
		float bgHeight = Art.background.getHeight() * scale;

		float maxScreenX = worldWidth - SCREEN_W;
		float maxScreenY = worldHeight - SCREEN_H;
		float maxBackgroundX = worldWidth - bgWidth;
		float maxBackgroundY = worldHeight - bgHeight;
		Vector2 scroll = new Vector2(maxBackgroundX / maxScreenX, maxBackgroundY / maxScreenY);

		batch.draw(Art.background, (ocam.position.x - SCREEN_W / 2) * scroll.x, (ocam.position.y - SCREEN_H / 2) * scroll.y, bgWidth, bgHeight);
		batch.enableBlending();

		// draw tiles
		for (int x = 0; x < tiles.length; x++) {
			for (int y = 0; y < tiles[0].length; y++) {
				int i = tiles[x][y] - 1;
				if (i == -1) {
					continue;
				}
				int xn = i % Art.tiles.length;
				int yn = i / Art.tiles[0].length;
				batch.draw(Art.tiles[xn][yn], x * TILE_SIZE, y * TILE_SIZE);
			}
		}

		for (Entity e : entities) {
			e.render();	// entity prepares itself for rendering

			// rendering one frame behind so we can have smooth interpolation
			t = new Vector2(e.ox, e.oy);
			t.lerp(new Vector2(e.x, e.y), interpolation);
			t.x = Math.round(t.x);
			t.y = Math.round(t.y);
			batch.draw(e.currentFrame, t.x, t.y, e.w, e.h);
		}

		for (Vector2 v : visualizer) {
			batch.draw(Art.hit, v.x, v.y);
		}

		batch.end();
	}
	// Collides Entity with the currently loaded level tiles.
	// Works with any sized entity* traveling at any speed.
	// Super efficient! (though some parts could be cleaned up still)
	// *: Still buggy when entities have exact dimensions of a tile
	public void collideWithMap(Entity e) {

		// Finds all possible tiles entity could hit this frame
		Vector2 tl = new Vector2();
		Vector2 br = new Vector2();
		if (e.x < e.ox) {
			tl.x = e.x;
			br.x = e.ox + e.w - 1f;
		} else {
			tl.x = e.ox;
			br.x = e.x + e.w - 1f;
		}
		if (e.y < e.oy) {
			br.y = e.y;
			tl.y = e.oy + e.h - 1f;
		} else {
			br.y = e.oy;
			tl.y = e.y + e.h - 1f;
		}

		int left = (int) (tl.x) / TILE_SIZE;
		int right = (int) (br.x) / TILE_SIZE;
		int top = (int) (tl.y) / TILE_SIZE;
		int bottom = (int) (br.y) / TILE_SIZE;

		left = Mth.clamp(left, 0, tiles.length - 1);
		right = Mth.clamp(right, 0, tiles.length - 1);
		top = Mth.clamp(top, 0, tiles[0].length - 1);
		bottom = Mth.clamp(bottom, 0, tiles[0].length - 1);

		ArrayList<Vector2> possibleTiles = new ArrayList<Vector2>();
		for (int x = left; x <= right; x++) {
			for (int y = bottom; y <= top; y++) {
				if (tiles[x][y] != 0) {
					possibleTiles.add(new Vector2(x * TILE_SIZE, y * TILE_SIZE));
				}
			}
		}

		visualizer.clear();
		visualizer.addAll(possibleTiles);

		// check collisions of all possible tiles you could collide
		// with this frame. do earlier collisions first.
		// this loop is kind of filthy and needs to be refactored
		int length = possibleTiles.size();
		for (int i = 0; i < length; i++) {
			Vector2 next = null;
			float nextColTime = Float.MAX_VALUE;

			for (Vector2 vec : possibleTiles) {
				Vector2 result = getNextCollisionTime(e, vec);
				if (result != null) {
					nextColTime = Math.min(result.x, nextColTime);
					if (nextColTime == result.x) {
						next = vec;
					}
				}
			}

			if (next != null) {
				Vector2 confirmed = getNextCollisionTime(e, next);
				Vector2 sep = new Vector2();
				sep.x = (1 - confirmed.x) * e.velx;
				sep.y = (1 - confirmed.x) * e.vely;
				if (confirmed.y == 0) {
					e.x -= sep.x;
					e.velx = 0f;
				} else {
					e.y -= sep.y;
					e.vely = 0f;
				}
			}
		}
		// was getting rounding errors with float comparision
		// i don't like this fix though
		e.x = Math.round(e.x);
		e.y = Math.round(e.y);
	}

	// checks to see if entity will collide with this tile
	// returns a vector2 with collision time and axis
	private Vector2 getNextCollisionTime(Entity e, Vector2 tileCoords) {
		Vector2 ret = null;
		float xl = tileCoords.x;
		float xr = tileCoords.x + TILE_SIZE;
		float yb = tileCoords.y;
		float yt = tileCoords.y + TILE_SIZE;

		// not sure which velocity to use here
		// Vector2 velocity = new Vector2(e.x - e.ox, e.y - e.oy);
		Vector2 velocity = new Vector2(e.velx, e.vely);
		Vector2 d = new Vector2();
		Vector2 dt = new Vector2();
		float firstCollision;
		float firstSeperation;
		boolean collided = false;

		if (velocity.x < 0) {
			d.x = xr - e.ox;
			dt.x = xl - (e.ox + e.w);
		} else {
			d.x = xl - (e.ox + e.w);
			dt.x = xr - e.ox;
		}

		if (velocity.y < 0) {
			d.y = yt - e.oy;
			dt.y = yb - (e.oy + e.h);
		} else {
			d.y = yb - (e.oy + e.h);
			dt.y = yt - e.oy;
		}

		d.div(velocity);
		dt.div(velocity);

		firstCollision = Math.max(d.x, d.y);
		firstSeperation = Math.min(dt.x, dt.y);

		collided = (firstCollision > firstSeperation) ? false : true;
		if ((d.x < 0f && d.y < 0f) || d.x > 1f || d.y > 1f || Float.isNaN(d.x) || Float.isNaN(d.y) || Float.isNaN(dt.x) || Float.isNaN(dt.y)) {
			collided = false;
		}
		if (collided) {
			// if the entity is exact same size as a tile this won't work (needs fixing)
			int axis = (firstCollision == d.x && firstCollision != d.y) ? 0 : 1;
			ret = new Vector2(firstCollision, axis);
		}

		return ret;
	}

	// brute force collisions, compare every entity against
	// every other entity. If any of them collide notify
	// both entities that the collision has occured
	// should try this with spatial hashing instead!
	public void checkCollisions() {
		for (int i = 0; i < entities.size(); i++) {
			for (int j = i + 1; j < entities.size(); j++) {
				Entity me = (Entity) entities.get(i);
				Entity him = (Entity) entities.get(j);

				if (me.collidesWith(him)) {
					me.collidedWith(him);
					him.collidedWith(me);
				}
			}
		}
	}

	// loads image into byte array of tiles
	// to verify which colors correspond with which tiles
	// you have to put a scan code thing on the top left
	public void loadLevel(String path) {
		Pixmap level = new Pixmap(Gdx.files.internal(path));
		tiles = new byte[level.getWidth()][level.getHeight()];
		worldWidth = tiles.length * TILE_SIZE;
		worldHeight = tiles[0].length * TILE_SIZE;

		// scans colors and puts into hashmap
		// overrides the color with the first pixels color (which will be air)
		HashMap<Integer, Byte> colors = new HashMap<Integer, Byte>();
		level.setColor(level.getPixel(0, 0));
		for (int i = 0; i < level.getWidth(); i++) {
			int color = level.getPixel(i, 0);
			level.drawPixel(i, 0);
			if (!colors.containsKey(color)) {
				colors.put(color, (byte) i);
			} else {
				break;
			}
		}

		// coverts weird color values into nice numbers using
		// the hashmap that was generated above using the scancode
		for (int y = 0; y < tiles[0].length; y++) {
			for (int x = 0; x < tiles.length; x++) {
				byte color = 0;
				if (colors.containsKey(level.getPixel(x, level.getHeight() - y - 1))) {
					color = colors.get(level.getPixel(x, level.getHeight() - y - 1));
				}
				tiles[x][y] = color;
			}
		}

		level.dispose();
	}
}
